Skip to content

Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference - #39

Open
coderdan wants to merge 30 commits into
v2from
cip-3328-supabase
Open

Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference#39
coderdan wants to merge 30 commits into
v2from
cip-3328-supabase

Conversation

@coderdan

@coderdan coderdan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Supabase integration section, plus the canonical encryptedSupabaseV3 reference. Targets the v2 docs branch.

Originally written against encryptedSupabase (the EQL v2 dialect). Reworked onto encryptedSupabaseV3 and the EQL 3.0.0 domains, verified against the adapter source on feat/eql-v3-text-search-schema and against the EQL v3 domain SQL rather than the PR descriptions, which have drifted.

Pages

  • /reference/stack/supabase — the canonical wrapper page. await encryptedSupabaseV3(url, key, options?) or (client, options?), connect-time introspection, .from(tableName), optional schemas for types and startup verification, the full builder surface, the domain-to-term table, response and error shapes, and the deferred-execution model. The v2 wrapper is documented at the foot.
  • /integrations/supabase — section overview.
  • /integrations/supabase/quickstart — empty project to encrypted, searchable table: install EQL, type the columns, wrap the client, query.
  • /integrations/supabase/fundamentals — migration vs SQL Editor install, role grants, functional indexes, RLS composition, pooler and introspection connections.
  • /integrations/supabase/auth — Supabase Auth session to OidcFederationStrategy, then .withLockContext().
  • /integrations/supabase/orms — PostgREST path vs a direct Postgres connection.
  • /integrations/supabase/edge-functions — the wasm-inline core client on the Edge runtime.

What the rework changed

  • Async constructor, because it introspects the database and derives each column's config from its EQL domain. No encryptedTable to keep in sync, and .from() takes only a table name.
  • Introspection needs a direct Postgres connection (DATABASE_URL or options.databaseUrl), a different credential from the anon key. Added to the quickstart env block.
  • schemas is optional: compile-time types plus startup verification, imported from @cipherstash/stack/eql/v3. There is no @cipherstash/stack/schema/v3 export.
  • select('*') works on v3, expanded from the introspected column list. It throws on v2.
  • Column types are the prefixed domains, public.eql_v3_text_eq, not eql_v3.text_eq. Domains live in public; functions and operators live in eql_v3.
  • Supabase runs the EQL installer as a non-superuser, so it cannot create the block-ORE operator class and disables the ORE-backed domains, which raise on first write. Called out on the pages that offer the choice.
  • .like() / .ilike() throw on encrypted columns. Free-text search is .contains().
  • The query builder's .withLockContext() takes a LockContext instance; the core client's takes a plain { identityClaim } object. The auth page was passing the core shape.

Known defect, documented

.contains() cannot match substrings today: text_search forces include_original: true, so the needle's bloom carries the whole needle as an extra token that the haystack's bloom cannot contain. Tracked in cipherstash/stack#610. A warn callout describes the behaviour; remove it when the fix lands.

Depends on

Notes for review

  • Anti-drift rule respected: EQL mechanics live in /reference/eql/*; these pages link rather than restate.
  • edge-functions.mdx still uses the v2 schema builders, because @cipherstash/stack/wasm-inline exports encryptedTable / encryptedColumn from @/schema and does not export the v3 types. Left alone; it already carries a "being confirmed" callout. Worth deciding whether the edge build should re-export the v3 surface.
  • Unverified claim carried over: that custom domain types don't appear in the Table Editor's type picker, so the SQL Editor is recommended. Needs a check against a live project.
  • Placeholders for forward-linked IA URLs still contain Linear URLs. Those are pre-existing across the v2 branch and are tracked separately; they must be stripped before anything reaches production.
  • bun run build passes. validate-content, validate-links, validate-mermaid pass.

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
public-docs Ready Ready Preview, 💬 2 unresolved
✅ 9 resolved
Jul 29, 2026 2:00am

Request Review

@coderdan
coderdan marked this pull request as ready for review July 2, 2026 12:56
…reference (CIP-3328)

- /reference/stack/supabase: THE canonical encryptedSupabase page —
  one signature ({encryptionClient, supabaseClient}, .from(table,
  schema)), full query-builder surface, schema-capability → EQL-term
  mapping, response/error shapes, deferred-execution model
- /integrations/supabase: flagship tutorial rewritten on EQL v3
  (install script → eql_v3 typed columns → wrapper → queries incl.
  ORDER BY on _ord; free-text as token containment, no LIKE)
- /integrations/supabase/{database,auth,dashboard-experience}:
  migrations-vs-SQL-Editor install, grants, indexes, RLS composition;
  Supabase Auth JWT → LockContext; Table Editor expectations + OAuth
  integration flow
- placeholder pages for 6 forward-linked IA URLs (identity-aware-
  encryption, schema-design, encrypt-existing-data, drizzle,
  audit-logging, cts) so no internal link 404s
- IA.md: tick CIP-3328 items; add CIP-3355 SDK-alignment gate
  (wrapper wire behaviour is still EQL v2 — blocks final merge)
Reorganize /integrations/supabase from 4 pages into: Overview (front
door from the marketplace listing), Quickstart (the tutorial, OAuth
folded in as optional), Fundamentals (database.mdx + dashboard-
experience.mdx merged), Data layer (ORM/framework router), Auth
(rebuilt on OIDC federation), and Edge Functions (new).

- Auth: replace the deprecated LockContext.identify() flow with
  OidcFederationStrategy as config.authStrategy, then
  .withLockContext({ identityClaim }) — lock context now correctly
  documented as requiring federation.
- Data layer: document the PostgREST vs direct-Postgres fork and
  Supabase pooler/connection glue; link out to canonical ORM pages
  rather than duplicating them.
- Edge Functions: written on the @cipherstash/stack/wasm-inline
  surface; wrapper-on-edge question gated behind a callout.
- Add Prisma + Next.js integration placeholders so Data-layer links
  resolve; fix stale /supabase/database link in the stack reference.
- Reorder meta.json; remove merged pages.
Addresses Vercel preview comment: the section folder and the index
page were both labelled 'Supabase', so the sidebar read 'Supabase ›
Supabase'. Title the index page 'Overview' to remove the repeat.
Replace the "How it fits together" section on the Supabase overview with
launch-article "How it works" copy. Fix EQL domain names against eql_v3
(int_ord -> integer_ord) and reframe text_search match as containment
matching (like LIKE '%term%'), with a caveat callout. Add Table Editor
screenshot.
- Remove em-dashes across the overview (AI "tell")
- Backtick `base` in "EQL base types"
- Convert the text_search note to a concise <Callout>
- Collapse the RLS/stack/KMS/performance/security sections into one
  linked "Built for the Supabase stack" summary
Add a shared, product-wide FAQ component (src/components/faq) that renders
an accordion and emits FAQPage JSON-LD. Generic answers (privacy, scaling,
KMS, RLS-vs-encryption, free tier) live once in faq/shared and are composed
with Supabase-specific questions inline on the overview. De-dupe the
performance/privacy prose now covered by the FAQ.
Reuse the shared privacy/KMS/free-tier FAQ entries and add DynamoDB-specific
questions (equality-only querying, keeping AWS SDK control, adoption).
Omits the Postgres-oriented scaling/RLS entries, which do not apply.
coderdan added 2 commits July 9, 2026 23:44
…ains

The Supabase pages documented `encryptedSupabase`, the EQL v2 dialect.
Stack 1.0 ships `encryptedSupabaseV3` alongside it, and it is a different
shape. Verified against the adapter source on the release branch, not
against the PR descriptions, which have drifted.

- `await encryptedSupabaseV3(url, key, options?)` or `(client, options?)`.
  It is async because it introspects the database at connect time, reads
  the EQL domain of every column, and derives the encryption config from
  it. There is no schema to declare and no `encryptedTable` to keep in
  sync, so `.from()` takes only a table name.
- Introspection needs a direct Postgres connection: `DATABASE_URL`, or
  `options.databaseUrl`. Documented in the quickstart env block.
- `schemas` is optional, and buys compile-time types plus startup
  verification. Import from `@cipherstash/stack/eql/v3`.
- `select('*')` works, expanded from the introspected column list. On v2
  it throws.
- `.like()` / `.ilike()` throw on encrypted columns. Free-text search is
  `.contains()`. With the default match options the query term carries
  the whole query string as a token, so a substring matches nothing;
  called out where it will bite.
- Column types are the EQL 3.0.0 prefixed domains: `public.eql_v3_text_eq`,
  not `eql_v3.text_eq`. The domains live in `public`; only the functions
  and operators live in `eql_v3`.
- Supabase runs the installer as a non-superuser, so the ORE-backed
  domains are disabled and raise on first write. Say so on the pages that
  offer the choice, and point at `_ord` / `text_search`.
- The query builder's `.withLockContext()` takes a `LockContext`
  instance, unlike the core client's, which takes a plain object. The
  auth page passed the wrong one.

The v2 wrapper is unchanged and still documented, at the foot of the
reference page.
@coderdan

coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Pushed two commits: a v2 sync merge, and a rework of the Supabase pages onto encryptedSupabaseV3 + the EQL 3.0.0 domains.

Everything below was verified against the adapter source on feat/eql-v3-text-search-schema (stack #588 merged into it, not into main) and against the EQL v3 domain SQL. The PR bodies have drifted from the code in three places, so I did not use them:

  • stack #588's body shows .like("email", "%@example.com") working. A later commit on the same branch, feat(stack)!: replace like/ilike with contains on the v3 supabase surface, makes .like() / .ilike() throw on encrypted columns.
  • EQL #389's body says text_search keeps [hm, ob, bf]. Its generated schema says [hm, op, bf].
  • main's encryptedSupabaseV3 is a sync ({encryptionClient, supabaseClient}) with a two-argument from(). That is the pre-#588 shape, because #588's base is #535, which is still open.

What changed

  • await encryptedSupabaseV3(url, key, options?) or (client, options?). Async, because it introspects at connect time and derives each column's config from its EQL domain. .from() takes only a table name; no encryptedTable to keep in sync.
  • Introspection needs a direct Postgres connection (DATABASE_URL or options.databaseUrl), which is a different credential from the anon key. Added to the quickstart env block, since it is easy to miss.
  • schemas is optional: compile-time types plus startup verification. Imported from @cipherstash/stack/eql/v3 (there is no @cipherstash/stack/schema/v3 export).
  • select('*') works on v3, expanded from the introspected column list. It throws on v2.
  • Column types are the prefixed domains, public.eql_v3_text_eq, not eql_v3.text_eq. Domains live in public; functions and operators live in eql_v3.
  • Supabase runs the EQL installer as a non-superuser, so it cannot create the block-ORE operator class and disables the ORE-backed domains, which then raise feature_not_supported on first write. Called out on the pages that offer the choice.
  • The query builder's .withLockContext() takes a LockContext instance; the core client's takes a plain { identityClaim } object. The auth page was passing the core shape to the builder. (Our own validate-content guard caught this, which is a nice result for chore(ci): gate the build on deprecated and non-existent API in docs #61.)

Two things worth a second opinion

.contains() may be unusable for substring search. text_search always builds its match index with include_original: true and exposes no tuning. The query term therefore carries the whole query string as one of its tokens, and containment requires every query token to be present in the stored set, so contains("email", "alice") finds nothing on a stored alice@example.com. The adapter's own note in types.ts says as much: "include_original: true is therefore always in force, so a substring contains matches nothing on either." I documented that behaviour in a warn callout, but if it is a bug rather than the intent, the callout should become a bug report.

edge-functions.mdx still uses the v2 schema builders, because @cipherstash/stack/wasm-inline exports encryptedTable / encryptedColumn from @/schema and does not export the v3 types. I left the page alone; it already carries a "being confirmed" callout. Worth deciding whether the edge build should re-export the v3 surface.

Depends on

  • The v3 domain registry re-prefix (in progress) — until it lands, encryptedSupabaseV3 rejects every EQL 3.0.0 table, because DOMAIN_REGISTRY is keyed on the old unprefixed names.
  • stack #609, for .in() on encrypted columns and .not(col, 'in', […]), both of which the filter table documents as working.
  • docs(eql): update the reference to the EQL 3.0.0 shapes #67, which moves the EQL reference to these same domain names.

Checks

bun run build passes. validate-content, validate-links, validate-mermaid pass.

The operand is a storage payload, not a query term: PostgREST cannot cast
a filter value to the narrow _query domain, and protect-ffi 0.28 rejects
encryptQuery for the v3 scalars. So the needle's bloom carries the whole
needle as an include_original token, which the haystack's bloom cannot
contain unless the two are equal, or the needle is exactly one trigram
long.

Says it is a defect to be fixed in EQL rather than intended behaviour.
@coderdan

coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my note above on contains. My explanation was wrong; the conclusion happened to be right, for a different reason. Pushed 6a16f79 to fix the callouts.

You're right about the storage side. include_original means the stored value's bloom carries its trigrams and the whole value as one extra token, so a query matches on the exact original term or on any of its trigrams. That is the intended widening.

What I missed is which payload the wrapper puts on the query side. It sends a full storage payload as the operand, not a narrow query term, and the source says why:

PostgREST has no syntax to cast a filter VALUE, and an uncast literal is ambiguous between the _query and jsonb @>/= overloads (42725). […] Independently, protect-ffi 0.28 throws EQL_V3_QUERY_UNSUPPORTED for any v3 scalar encryptQuery, so a narrowed term cannot be produced today.

Confirmed at query-builder-v3.ts:356, which calls encryptionClient.encrypt(...) rather than encryptQuery(...).

So the needle gets tokenized as if it were a stored value: trigrams("alice") plus include_original("alice"). Containment requires every needle bit to be present in the haystack. The trigrams are there, but the whole-needle token "alice" is not among the haystack's tokens, which are trigrams("alice@example.com") plus "alice@example.com". So containment fails.

It follows that .contains() matches only when the needle equals the stored value, or when the needle is exactly three characters, where its include_original token is one of the haystack's trigrams.

The class doc reaches the same conclusion and calls it a defect:

KNOWN BROKEN for real substrings, and not fixable from this file. […] v3 Drizzle's contains has the same defect for the same reason. Tracked in EQL; do not paper over it here.

The docs now describe it as a known defect with that precise rule, rather than as intended behaviour. I could not find the tracking issue in encrypt-query-language (searched open and closed), so it may be in Linear.

Drizzle and the Supabase wrapper share the defect but not the cause. The
common blocker is that protect-ffi has no v3 scalar encryptQuery, so no
client can build a narrow operand. Only Supabase is additionally blocked
by PostgREST's inability to cast a filter value to the query domain;
Drizzle emits its own SQL and can cast once a narrow term exists.
Not a protect-ffi gap: substring containment works at the ffi layer when
include_original is off, which is the schema default. stack's shared
match defaults force it on, inherited from the v2 builder, where QueryMode
dropped the original token. Link the tracking issue.
@coderdan coderdan changed the title Docs V2: Supabase integration section + canonical encryptedSupabase reference (CIP-3328) Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference Jul 9, 2026
The Vercel build failed in `generate-docs`. Three causes, all from the
Stack 1.0 monorepo restructure:

1. Stale entry points. `secrets/`, `drizzle/`, and `supabase/` no longer
   exist under `packages/stack/src` (secrets removed; the two adapters
   moved to their own packages), so TypeDoc had nothing to read. Dropped
   them; kept the seven modules `@cipherstash/stack` still ships.

2. Type errors from cross-package resolution. The stack monorepo is a
   pnpm workspace, but the generator installs it with `bun`, which leaves
   references like `@cipherstash/protect-ffi`'s `ProtectError` unresolved
   for the isolated typecheck even though 0.29.0 exports it. Set TypeDoc's
   `skipErrorChecking: true` — it emits accurate signatures from the source
   AST regardless, which is the right posture for documenting external
   source we don't control.

3. Multi-version generation broke on the layout change. The module layout
   diverged across majors (0.18 still has the adapters/secrets; 1.0 does
   not), and one shared `entryPoints` config can't document both. Generate
   only the latest major. Also reset the working tree before each checkout,
   since a prior tag's `bun install` rewrites the tracked package.json and
   would block switching tags.

Also strips the now-dead TypeDoc deep-links from the legacy Drizzle
reference page (the adapter's API pages are no longer generated here) and
points it at /integrations/drizzle.

Verified: `generate-docs`, `validate-links`, and a full `bun run build`
all pass.
@coderdan

Copy link
Copy Markdown
Contributor Author

Fixed the failing Vercel build (79c726e). It was the TypeDoc SDK-reference generator choking on the Stack 1.0 monorepo restructure, in three ways:

  1. Stale entry pointssecrets/, drizzle/, and supabase/ no longer exist under packages/stack/src (secrets removed; the two adapters became their own packages), so TypeDoc had nothing to read. Kept the seven modules @cipherstash/stack still ships.
  2. Cross-package type errors — the stack monorepo is a pnpm workspace but the generator installs it with bun, which leaves e.g. @cipherstash/protect-ffi's ProtectError unresolved for the isolated typecheck (0.29.0 does export it). Set TypeDoc skipErrorChecking: true — it emits accurate signatures from the source AST regardless.
  3. Multi-version generation — the module layout diverged across majors, and one entryPoints config can't document both 0.18 and 1.0. Now generates only the latest major; older-major reference lives on the previous docs site. Also resets the working tree before each tag checkout, since a prior tag's bun install rewrites the tracked package.json.

Verified locally: generate-docs, validate-links, and a full bun run build all pass.

This is a shared generator fix. #43 and #54 will hit the same red build until they get it — either after this merges to v2 and they rebase, or I can cherry-pick 79c726e onto them now. Say the word.

coderdan added 2 commits July 28, 2026 18:10
# Conflicts:
#	scripts/lib/docs-generator.ts
#	src/mdx-components.tsx
Stack renamed the encrypted free-text method in
cipherstash/stack#617 (`feat(stack-drizzle,stack-supabase)!: rename encrypted
contains() -> matches()`), landed 2026-07-13 — one day before this branch was
last updated, so it was missed.

The rename is not cosmetic. `contains()` now RAISES on an encrypted text
column, because containment promises exactness that bloom-filter matching
cannot deliver:

  contains() is native (exact) containment and does not apply to encrypted
  column "email". Use matches() for encrypted free-text search.

Every free-text example on these pages taught the spelling that now raises.
`contains()` is still correct for encrypted JSON (sub-document containment)
and plaintext arrays, so the filter table now lists both, plus the
selectorEq/selectorNe pair added alongside them.

Also rewrites "There is no `.like()`", which was wrong in the other direction.
`like`/`ilike` on an encrypted column do not throw — they are a compatibility
shim that reduces the pattern to a needle and delegates to `matches()`. The
result is approximate (case-insensitive, one-sided, anchoring lost) and only
an unapproximable pattern — an internal `%`, or any `_` — raises. Plaintext
columns keep real SQL LIKE. The section now says that, and the anchor is
renamed to match, with the inbound link updated.

Verified against @cipherstash/stack@1.0.0-rc.4 (packages/stack-supabase):
encryptedSupabaseV3's four overloads are async and take (client | url+key) plus
options, and databaseUrl/config/schemas are all real, so the surrounding
examples were already correct. The v2 paragraph checks out too — encryptedSupabase
is synchronous, takes { encryptionClient, supabaseClient }, and needs .from(table,
schema).

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderdan

Copy link
Copy Markdown
Contributor Author

Rebased onto current v2 and reviewed the Supabase surface against @cipherstash/stack@1.0.0-rc.4. Now MERGEABLE, full bun run build green (732 pages, no broken links, EQL drift clean).

Conflicts (2, both straightforward)

  • scripts/lib/docs-generator.ts — this branch's fix(generator): make the TypeDoc SDK reference build against Stack 1.0 introduced skipErrorChecking: true. docs: resolve protect-ffi's real types so the Stack reference stops saying any #77 has since superseded that: the real cause was a missing customConditions: ["node"], and tolerating the errors was making TypeDoc emit any into 13 published types. Took v2's side; this branch's workaround is no longer needed.
  • src/mdx-components.tsx — purely additive on both sides (Faq here, EqlFn on v2). Kept both.

API drift — one breaking rename, missed by one day

cipherstash/stack#617feat(stack-drizzle,stack-supabase)!: rename encrypted contains() → matches() — landed 2026-07-13. This branch was last updated 2026-07-14, so it just missed it.

It is not cosmetic. contains() now raises on an encrypted text column:

contains() is native (exact) containment and does not apply to encrypted
column "email". Use matches() for encrypted free-text search.

All six free-text examples and prose references taught the spelling that now raises. Fixed in d6d7ece. contains() is still correct for encrypted JSON (sub-document containment) and plaintext arrays, so the filter table now lists both — plus selectorEq() / selectorNe(), which arrived with encrypted-JSON querying on 2026-07-15.

"There is no .like()" was wrong the other way

The section claimed .like() / .ilike() throw on encrypted columns. They don't — they're a deliberate compatibility shim that reduces the pattern to a needle and delegates to matches():

like/ilike on an ENCRYPTED column are a best-effort compatibility shim, delegated to matches. […] the result is APPROXIMATE — matching is case-insensitive and one-sided (may false-positive), and anchoring is lost. Leading/trailing % are stripped; an internal % or any _ cannot be approximated by trigram matching and throws.

Rewritten to say that, with the anchor renamed and the inbound link from the quickstart updated.

What checked out unchanged

  • Package @cipherstash/stack-supabase and the encryptedSupabaseV3 name are correct — it's four async overloads taking (client | url + key) plus options, so the await in every example is right.
  • Options databaseUrl, config, schemas are all real fields of EncryptedSupabaseV3Options.
  • The v2 paragraph is accurate: encryptedSupabase is synchronous, takes { encryptionClient, supabaseClient }, needs .from(table, schema), and its like/ilike do route through an encrypted pattern filter.
  • withLockContext, audit, order, range, csv, single/maybeSingle, throwOnError, abortSignal all exist on the builder.

Two follow-ups, not in this PR

  • Encrypted-JSON querying is undocumented. contains() (JSON form), selectorEq(), selectorNe(), and filter(col, 'cs', …) landed 2026-07-15 and are a real feature area. I added them to the filter table so the page isn't misleading, but they deserve prose and examples — probably their own page under integrations/supabase/.
  • validate-content-api couldn't catch this. It passed with "✓ no deprecated or non-existent API found" while the docs taught a method that raises. It's a hand-maintained denylist of specific patterns (.identify(, eql_v2_encrypted, …), not a real API check. A contains()-as-free-text rule would guard this exact regression — its file header describes precisely this failure mode ("a page fixed on one branch can be silently reintroduced by a port from the other") — but the pattern needs care, since .contains() is legitimate for JSON and arrays.

coderdan added 2 commits July 28, 2026 19:25
… adapter

Correcting my own earlier commit. Renaming contains() -> matches() fixed the
spelling but documented an operation that does not run: on EQL 3.0.2+ the
supabase-js wrapper raises for every encrypted free-text and encrypted-JSON
term, whatever the spelling.

  [supabase v3]: matches() on encrypted column "email" is unavailable with EQL
  3.0.2+: the SQL operator requires an eql_v3.query_* cast that PostgREST
  cannot express. Use the Drizzle or Prisma Next adapter, or a scoped SQL/RPC
  function.

The docs pin EQL 3.0.4, so this is the behaviour every reader gets. The guard
fires for the freeTextSearch and searchableJson query types via any spelling —
matches(), contains() on a json column, selectorEq()/selectorNe(), and the same
terms reached through not(), or(), or a raw filter() — and fails closed on an
unknown EQL version. It does NOT touch equality, ordering, or range, so the
rest of the page stands.

Scoped to the supabase-js query path, which is what the limit is about. The
Auth and Edge Functions pages are unaffected: this is PostgREST's inability to
express an operand cast, not a limit on CipherStash with Supabase.

- The wrapper reference leads the free-text section with the constraint and the
  three workarounds, and marks the affected rows in the filter table.
- The quickstart drops its free-text example rather than showing a call that
  raises, and its callout now says which operations do work.
- fundamentals' column-type table is accurate as written (these ARE the
  column's capabilities) but a reader could pick text_match or json and only
  then discover they cannot query it here, so it points at the constraint.

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderdan

Copy link
Copy Markdown
Contributor Author

Synced to current v2 (fa5dc67) — picks up #80, so the generated stack-supabase reference now exists alongside this hand-written page. Clean merge, no conflicts, and the auto-merge is coherent: the stack-supabase entry point and the derived workspace paths both carried through. 789 pages build.

Correcting my earlier commit

d6d7ece renamed contains()matches(), which fixed the spelling but documented an operation that doesn't run. I found this while answering a question about #80's TSDoc, and it's the more important half of the review:

[supabase v3]: matches() on encrypted column "email" is unavailable with EQL
3.0.2+: the SQL operator requires an eql_v3.query_* cast that PostgREST cannot
express. Use the Drizzle or Prisma Next adapter, or a scoped SQL/RPC function.

These docs pin EQL 3.0.4, so that's what every reader gets. The guard fires for the freeTextSearch and searchableJson query types through any spelling — .matches(), .contains() on a json column, .selectorEq() / .selectorNe(), and the same terms reached via .not(), .or(), or a raw .filter(). It also fails closed on an unknown EQL version.

It does not touch equality, ordering, or range. .eq(), .neq(), .in(), .gt()/.gte()/.lt()/.lte(), .order(), inserts, updates and decrypt-on-select are all fine, so the rest of the page stands as written.

Scoped deliberately to the supabase-js query path — this is PostgREST's inability to express an operand cast, not a limit on CipherStash with Supabase. Auth and Edge Functions are untouched.

Page Change
reference/stack/supabase.mdx Free-text section now leads with the constraint and the three workarounds; affected rows marked in the filter table
integrations/supabase/quickstart.mdx Dropped the free-text example rather than showing a call that raises; callout says which operations do work
integrations/supabase/fundamentals.mdx Column-type table is accurate as written, but a reader could pick text_match or json and only then find they can't query it here — now points at the constraint

Note for review

The .contains().matches() rename in d6d7ece is still correct and worth keeping: on an encrypted text column contains() raises on every EQL version, independently of this. The two commits are separable — d6d7ece fixes which method is named, 445a615 fixes whether it runs.

bun run build green: no broken links (both new anchors resolve), EQL drift clean, all validators pass.

…s remove_v2

Two Vercel toolbar comments on the preview.

edge-functions: generate the CS_* values with `stash env` rather than having the
reader assemble the file by hand. Additive — the explicit variable listing
stays, since it is what documents which four the SDK actually reads. `--write`
is a boolean (verified against stash@0.17.1), so the example redirects to the
env file that `supabase functions serve --env-file` then consumes.

fundamentals: rewrite the install callout against the stack `remove_v2` branch.
What was out of date:

- It justified the v3 restrictions as flags that "exist for the v2 install
  paths". With v2 removed there are no v2 install paths, and the install is
  always v3, so the framing goes.
- It listed `--drizzle` alongside the other rejected flags. `--drizzle` has had
  its own v3 answer since stack#691: `stash eql migration --drizzle` generates a
  v3 install migration, cs_migrations tracking schema included, for Drizzle
  projects that want the install in their migration history. The install command
  still rejects the flag, but now points there rather than at `--eql-version 2`.
- Also drops "`--eql-version 2` opts back to v2" from the prose above it.

`--migration`, `--migrations-dir`, and `--latest` remain rejected, and there is
still no Supabase migration variant — one v3 SQL artifact targets every
platform — so the `supabase db reset` consequence stands and is kept.

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderdan

Copy link
Copy Markdown
Contributor Author

Both Vercel toolbar comments addressed in cb590a9 (replied on each thread).

  • Edge Functionsstash env added additively; the explicit CS_* listing stays, since it's what documents which four the SDK reads. Verified --write against stash@0.17.1: it's a boolean with no path argument, so the example redirects into the file supabase functions serve --env-file consumes.
  • Fundamentals — install callout rewritten against remove_v2. Dropped the "they exist for the v2 install paths" framing and the --eql-version 2 line; moved --drizzle out of the rejected list and pointed it at stash eql migration --drizzle (stack#691), whose source comment is explicit that steering users to v2 is the wrong advice. --migration, --migrations-dir, --latest stay rejected and the supabase db reset consequence stands.

A bigger decision this turned up

remove_v2 renames the whole public surface, and this PR is written against the current one:

now (1.0.0-rc.4) on remove_v2
encryptedSupabaseV3(...) encryptedSupabase(...) — same async overloads
EncryptedSupabaseV3Options EncryptedSupabaseOptions
EncryptedSupabaseV3Instance EncryptedSupabaseInstance
TypedEncryptedSupabaseV3Instance TypedEncryptedSupabaseInstance

query-builder-v3.ts is gone as well, and the EQL 3.0.2+ free-text/JSON guard survives (moved to query-encrypt.ts, same message), so that callout is unaffected either way.

Two consequences for this PR:

  1. encryptedSupabaseV3 appears throughout — quickstart, auth, edge-functions, the section index, and the reference page's title and anchors. All of it becomes wrong on remove_v2.
  2. The "The v2 wrapper" section is worse than wrong: it documents encryptedSupabase as the v2 dialect, and that name will mean the v3 one. Left as-is it actively misleads.

I haven't done the rename, because of a conflict worth your call: the generated reference builds from the latest published tag, which is 1.0.0-rc.4 — still encryptedSupabaseV3. Renaming the hand-written pages now puts them in direct contradiction with the generated pages sitting next to them, until remove_v2 ships and the tag moves.

Options as I see them:

  • Rename now and accept a window where hand-written and generated pages disagree. Consistent with "finish both before going live", and the disagreement resolves itself on release.
  • Rename when remove_v2 ships, as one commit, and keep this PR internally consistent with the current published SDK meanwhile.
  • Rename now and pin the generated reference to a remove_v2 build, so both sides agree — more moving parts, since the tag doesn't exist yet.

Happy to do whichever. The rename itself is mechanical; deleting or rewriting the v2-wrapper section is the only part needing judgement.

@coderdan

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment: encryptedSupabaseV3 is not gone on remove_v2. I read the new encryptedSupabase overloads and concluded the old name had been dropped. It hasn't — it's kept as a deprecated, type-identical alias:

/**
 * @deprecated Use {@link encryptedSupabase}. `encryptedSupabaseV3` is a
 * type-identical alias kept for existing imports; the `V3` suffix is redundant
 * now that EQL v3 is the only generation this wrapper authors.
 */
export const encryptedSupabaseV3 = encryptedSupabase

The types are aliased the same way — EncryptedSupabaseV3Options, FilterableKeys, FreeTextSearchableKeys, EncryptedFreeTextKeys, SearchableJsonKeys and friends all have @deprecated Use {…} shims.

So there is no rename deadline, and no window of contradiction. Every encryptedSupabaseV3 example in this PR keeps compiling and working after remove_v2 ships. The rename becomes optional polish — worth doing eventually to steer readers off a deprecated name, but it can happen any time, including well after the branch lands. Scratch the three options I laid out; none of them are needed.

What I verified, and how — usable now, without waiting for a tag

PROTECT_WORKSPACE_PATH makes the generator read a local checkout instead of resolving the latest published tag, so we can build the reference against remove_v2 today:

PROTECT_WORKSPACE_PATH=/path/to/stack-remove-v2 bun run generate-docs

That ran clean — 0 TypeDoc errors — and produced variables/encryptedSupabase.mdx plus a struck-through encryptedSupabaseV3.mdx marked deprecated, which is how I found the alias. Anything else on this PR can be checked the same way before the branch merges.

The one thing that IS wrong at launch

The "The v2 wrapper" section. encryptedSupabase is documented there as the EQL v2 dialect, and after remove_v2 that name is the v3 wrapper — so the section describes the opposite of what it names. Unlike the rename, this doesn't self-resolve, and since these docs go live after remove_v2, the v2 dialect it describes will never have existed for a reader of the live site.

That's a small, self-contained edit: delete the section and drop the "Pick the dialect by function" framing, since after the merge there is only one. Happy to do it now — it's correct against remove_v2 and harmless if the branch slips, because the section is only reachable on an unreleased docs branch either way.

Also worth noting from the same check: the EQL 3.0.2+ free-text/JSON guard survives on remove_v2 (moved to query-encrypt.ts, byte-identical message), so the constraint callout added earlier is right for both the current and target state.

…stead

`remove_v2` makes `encryptedSupabase` the v3 wrapper, so a section documenting
it as "the EQL v2 dialect" would describe the opposite of what it names. The v2
dialect it described will never have existed for a reader of the live site,
since these docs ship after that branch.

Replaced with a short note on the naming, because the alias is the thing a
reader actually needs: `encryptedSupabase` and `encryptedSupabaseV3` are the
same function, either import works, and the V3 spelling is a deprecated alias
kept for existing imports now that the suffix is redundant. Same for the type
names. Also drops the "pick the dialect by function" framing — after the merge
there is only one dialect.

The examples on this page still say `encryptedSupabaseV3`, deliberately. That
name is correct both today and after the merge, whereas `encryptedSupabase`
means the OLD synchronous v2 wrapper on the currently published SDK
(1.0.0-rc.4) — switching the examples now would document a different function
with a different signature. They should be renamed once remove_v2 ships, along
with this page's `verifiedAgainst`.

Also fixes one cell in choose-your-stack, which is outside this PR's scope but
factually wrong in the same way: it advertised `.like()` as something that
"keeps working" on Supabase. It delegates to `matches()`, which raises on EQL
3.0.2+ through PostgREST. Swapped for `.order()`, which does.

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderdan

Copy link
Copy Markdown
Contributor Author

v2-wrapper section fixed in 544679c.

Removed the section documenting encryptedSupabase as "the EQL v2 dialect" — after remove_v2 that name is the v3 wrapper, so it described the opposite of what it named, and the dialect it described will never have existed for a reader of the live site.

Replaced with a short note on the naming, since the alias is what a reader actually needs: the two names are the same function, either import works, and encryptedSupabaseV3 is a deprecated alias now the suffix is redundant. Same for the type names. The "pick the dialect by function" framing is gone — after the merge there's only one.

Why the examples still say encryptedSupabaseV3

Deliberate, and it's the one thing that stopped me renaming the page wholesale. That name is correct both today and after the merge. encryptedSupabase is not:

1.0.0-rc.4 (published) after remove_v2
encryptedSupabase v2 wrapper — synchronous, { encryptionClient, supabaseClient }, .from(table, schema) v3 wrapper, async
encryptedSupabaseV3 v3 wrapper, async same function, deprecated alias

Renaming the examples now would point readers at a different function with a different signature on the SDK that's actually published. So the page keeps the spelling that's true in both worlds, and the note explains the discrepancy rather than pretending it away.

Follow-up when remove_v2 ships: rename the examples to encryptedSupabase across this page, the quickstart, auth, edge-functions and the section index, and update this page's verifiedAgainst (currently stack: 1.0.0-rc.0, already behind). Mechanical, and verifiable before the tag exists with PROTECT_WORKSPACE_PATH=/path/to/stack-remove-v2 bun run generate-docs.

One out-of-scope fix, flagged

get-started/choose-your-stack.mdx isn't part of this PR, but its Supabase row advertised .like() as something that "keeps working". It delegates to matches(), which raises on EQL 3.0.2+ through PostgREST — the same error I documented elsewhere in this PR, on a getting-started page. Swapped .like() for .order(), which does work. One cell; happy to split it out if you'd rather keep this PR clean.

Left alone, needs a decision

reference/agent-skills.mdx describes the stash-supabase skill as covering filters "eq, neq, like, ilike, gt, …" — no matches, no contains/selectorEq. That list looks stale against the v3 surface, but it documents what the skill covers rather than teaching the API, and checking it properly means reading the ~800-line skills/stash-supabase/SKILL.md on remove_v2 and confirming what it actually teaches. Worth doing, but it's a separate pass — say the word and I'll take it.

bun run build green: no broken links, all validators pass.

… Data layer away

Four structural changes to the Integrations section:

- The /integrations landing page leaves the sidebar (`"!index"` in
  meta.json, which drops it as both a child item and the folder link).
  It stays reachable from the docs index, Get started, and the v2
  redirect map — it just isn't a nav entry.

- The landing page becomes a plain list by category: Database (Supabase,
  Prisma Postgres) and ORM (Supabase-js, Prisma Next, Drizzle). The
  decision content it used to duplicate — the Postgres platform table,
  the ORM table, the identity-provider table — already lives on
  /get-started/choose-your-stack, which it now points at instead.

- Supabase Overview and Fundamentals merge into one Overview page. The
  overlap was substantial: both explained EQL column types, both covered
  RLS, and both explained why the Table Editor can't create encrypted
  columns. Fundamentals' section anchors are preserved on the merged
  page, so #installing-eql, #row-level-security,
  #working-in-the-dashboard and #connecting-your-project-to-cipherstash
  keep resolving; inbound links across auth, quickstart and the wrapper
  reference are repointed.

- Data layer folds into the Quickstart as "Using an ORM instead",
  condensed from 71 lines to ~20: what the direct-connection path is,
  which Supabase connection string to use, and the fact that a
  privileged direct connection bypasses RLS.

Verified against the rendered output rather than the source: the built
sidebar has no bare /integrations link, and every anchor the merge moved
resolves to a real id on the merged page.

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderdan

Copy link
Copy Markdown
Contributor Author

Pushed 9f7e2de — the four structural changes.

1. /integrations out of the sidebar. meta.json now says "!index". Worth knowing why that exact spelling: in Fumadocs a folder's index.mdx is attached as the folder's own link before pages is processed, so merely dropping "index" from the list doesn't remove it — it promotes it to the folder header instead. !index is what removes it from both positions. The page still exists and is still reachable from the docs index, Get started, and the v2 redirect map (/integrations is a redirect destination, so it had to stay).

2. Landing page is now a list by category. Database (Supabase, Prisma Postgres) and ORM (Supabase-js, Prisma Next, Drizzle), and not much else.

Two link choices worth a look, since neither entity had a page of its own:

  • Prisma Postgres/integrations/prisma-next. There's no Prisma Postgres content anywhere in the repo, and writing a page for it wasn't part of "simply list each integration". The Prisma Next page is the only thing that covers running EQL on it, so that's where the card points.
  • Supabase-js/reference/stack/supabase, the wrapper's canonical page, so the two Supabase entries land somewhere different from each other.

I also dropped three tables from that page — Postgres platforms, ORMs, identity providers. They weren't lost: all three are already on /get-started/choose-your-stack, verbatim, which the page now links to at the bottom. That duplication is arguably the reason the landing page needed simplifying.

3. Overview + Fundamentals merged. You were right that they overlapped: both explained EQL column types (Fundamentals' table is better, so that's the one that survived), both covered RLS, and both explained the Table Editor's custom-domain problem. Net −130 lines against a straight concatenation.

The four Fundamentals anchors are preserved on the merged page, so #installing-eql, #row-level-security, #working-in-the-dashboard and #connecting-your-project-to-cipherstash still resolve. Inbound links from auth, quickstart and the wrapper reference are repointed.

4. Data layer folded into the Quickstart as ## Using an ORM instead, 71 lines → ~20. Kept the two things that are genuinely Supabase-specific and not documented elsewhere: which connection string (direct 5432 vs Supavisor 6543) and the fact that a privileged direct connection bypasses RLS, which the PostgREST path doesn't. Dropped the two-paths table and its own "where to next".


How I checked it. validate-links.ts only covers content/stack, so nothing in the repo would have caught a broken link or a dead anchor in content/docs — which is most of what this change could break. I wrote a throwaway checker for the v2 tree and confirmed the 10 remaining failures are identical before and after this commit (all pre-existing; two categories are false positives of my checker — route handlers like /llms.txt, and static assets under /images/).

Then I checked the built HTML rather than the source: no bare /docs/integrations link in any sidebar, the Supabase section renders as Overview → Quickstart → Supabase Auth → Edge Functions, and all five moved anchors resolve to real ids on the merged page. Build is green at 785 pages; biome check is clean on everything I touched (the two files it does flag, scripts/generate-cli-docs.ts and src/components/code-block.tsx, are untouched and were already failing).

One thing to note: bun run build rewrites a number of tracked generated files on this branch — the CLI reference, reference/eql/functions.mdx, src/lib/eql-version.ts, and ~40k lines of content/stack/reference/eql/index.mdx. I reverted all of it, so this commit is only the 9 files above. It's a sign this branch's EQL pin is behind v2, which the rebase would settle.

Two things I left alone, deliberately:

  • get-started/choose-your-stack.mdx points at /stack/cipherstash/encryption/drizzle and .../prisma-next — legacy-tree paths — when /integrations/drizzle and /integrations/prisma-next exist in v2. Real bug, but not this PR's.
  • reference/eql/v2/* links to three anchors on /stack/.../searchable-encryption that don't exist. Pre-existing, in the retained v2 reference.

Say the word on either and I'll pick them up.

`content/stack/reference/drizzle.mdx` points at `/integrations/drizzle`,
which is a page this branch creates. #81 (the link-validator fix, off v2)
removes that link, because on v2 alone it is a genuine 404 — the target
does not exist there yet.

That set up a silent failure. This branch did not touch the line, so
rebasing onto a v2 containing #81 would have taken #81's removal without
a conflict, dropping the cross-link even though the page it points at had
just landed.

Rewording the sentence here makes the branch an explicit editor of that
line, so the two changes conflict and the merge has to make a decision
instead of quietly picking one. The rewrite is also more accurate on its
own terms: this page documents the older `@cipherstash/stack/drizzle`
entry point, which the previous phrasing left implicit.

The redirect itself needed no change — `/stack/reference/drizzle` →
`/integrations/drizzle` has been in v2-redirects.mjs all along. What this
branch supplies is the destination, so the redirect resolves here where
it did not on v2.

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderdan

Copy link
Copy Markdown
Contributor Author

Pushed fbc655f. Short answer: the redirect was already set — nothing needed adding — but checking it turned up a silent-failure mode between this PR and #81, which is what the commit fixes.

The redirect. /stack/reference/drizzle/integrations/drizzle has been in v2-redirects.mjs (line 343) all along, and it's unchanged on this branch. What this branch supplies is the destination. I checked all 71 entries in the map against the page tree on this branch: 19 have destinations that don't resolve yet, and /integrations/drizzle is not one of them — it resolves here, where it doesn't on v2. (The other 19 are the known stubs; validate-v2-redirects.ts says in its own docstring that it checks map coverage, not destination existence, and leaves that to CIP-3335.)

The problem worth fixing. content/stack/reference/drizzle.mdx:134 links to /integrations/drizzle in prose. On v2 that's a real 404, so #81 removes it. This branch never touched that line — so rebasing onto a v2 containing #81 would have taken the removal without a conflict, quietly dropping the cross-link at the exact moment the page it points to landed.

I verified both halves of that with git merge-tree rather than assuming:

So the merge now has to make a decision. The reword is also more accurate on its own terms — this page documents the older @cipherstash/stack/drizzle entry point, which the previous phrasing left implicit:

The Drizzle adapter now ships as its own package, @cipherstash/stack-drizzle, with the EQL v3 dialect on @cipherstash/stack-drizzle/v3. This page documents the older @cipherstash/stack/drizzle entry point; the Drizzle integration is the current, EQL v3 surface:

On merge order, either way works now:

Build is green at 785 pages, validate-redirects passes (85 legacy pages covered).

# Conflicts:
#	content/stack/reference/drizzle.mdx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant